#include <cb/utility/archive.hpp>
#include <functional>
// g++: ranges lib
#include <algorithm>

CB_NAMESPACE_BEGIN

void Archive::ClearArchive() {
    blobs_.clear();
    current_domain_ = "";
    modified_and_not_saved_ = true;
}

void Archive::DropDomain(const std::string_view &anydomain) {
    if(IsRelativeDomain(anydomain)) {
        auto it = blobs_.find(GetSubdomainPath(anydomain));
        if (it != blobs_.end()) {
            blobs_.erase(it);
        } else {
            // TODO add logger
            // LOG_WARN("Domain {} does not exist.", subdomain);
        }
    } else {
        auto it = blobs_.find(std::string(anydomain));
        if (it != blobs_.end()) {
            blobs_.erase(it);
        } else {
            // TODO add logger
            // LOG_WARN("Domain {} does not exist.", subdomain);
        }
    }
    modified_and_not_saved_ = true;
}
// Merge blobs.
struct Archive::MergedBlob {
    size_t total_binary_size;
    std::vector<std::shared_ptr<Archive::Blob> > blobs;
    void Serialize (std::ostream & os) const {
        auto blob_count = blobs.size();
        os.write(reinterpret_cast<const char *>(&blob_count), sizeof(blob_count));
        for(const auto & blob : blobs) {
            blob->Serialize(os);
        }
    }
    void Deserialize (std::istream & is) {
        size_t blob_count;
        is.read(reinterpret_cast<char *>(&blob_count), sizeof(blob_count));
        blobs.resize(blob_count);
        for(auto & blob : blobs) {
            blob = std::make_shared<Archive::Blob>();
            blob->Deserialize(is);
        }
    }
};

bool Archive::StoreArchiveToDirectory(const std::filesystem::path &path, bool overwrite) {
    std::error_code ecode;
    if(!create_directories(path, ecode)) {
        // TODO add logger
        // LOG_WARN("Failed to create directory {}. Error code: {}.", path, ecode.messsage());
        return false;
    }
    if(!is_empty(path) && !overwrite) {
        // TODO add logger
        // LOG_WARN("Directory {} already exists. Overwrite is not allowed.", path);
        return false;
    }

    std::vector<std::shared_ptr<MergedBlob> > merged_blobs;
    uint32_t individual_blob_count = 0;
    for(const auto &kv : blobs_) {
        const auto & blob = kv.second;
        if(blob->header.options.can_merge_on_store) {
            if(merged_blobs.empty()) merged_blobs.emplace_back();
            if(merged_blobs.back()->total_binary_size + blob->GetBinarySize() > C::kMaxMergedBinarySizeBytes) {
                merged_blobs.emplace_back();
            }
            auto & merged_blob = merged_blobs.back();
            merged_blob->blobs.emplace_back(blob);
        } else individual_blob_count ++;
    }
    if(individual_blob_count > 1024) {
        // TODO add logger
        // LOG_WARN("There are {} individual blobs. This may cause performance issues.", individual_blob_count);
    }
    try {
        // Writing binary files for merged blobs
        uint32_t merged_blob_count = 0;
        for (const auto &merged_blob: merged_blobs) {
            std::stringstream ss;
            merged_blob->Serialize(ss);
            auto binary = ss.str();
            auto binary_name = "Merged_" + std::to_string(++merged_blob_count) + ".pack";
            auto binary_path = path / binary_name;
            std::ofstream ofs(binary_path, std::ios::binary);
            if (ofs.bad()) {
                // TODO add logger
                // LOG_WARN("Failed to open file {} for writing.", binary_path);
                return false;
            }
            ofs.write(binary.data(), binary.size());
        }
        // Writing binary files for non-merged blobs
        for (const auto &kv: blobs_) {
            const auto &blob = kv.second;
            if (!blob->header.options.can_merge_on_store) {
                std::stringstream ss;
                blob->Serialize(ss);
                auto binary = ss.str();
                auto binary_name = kv.first + ".bin";
                auto binary_path = path / binary_name;
                auto binary_directory = binary_path.parent_path();
                if (!create_directories(binary_directory, ecode)) {
                    // TODO add logger
                    // LOG_WARN("Failed to create directory {}. Error code: {}.", binary_directory, ecode.messsage());
                    return false;
                }
                std::ofstream ofs(binary_path, std::ios::binary);
                ofs.write(binary.data(), binary.size());
            }
        }
    } catch (const std::exception &e) {
        // TODO add logger
        // LOG_WARN("Failed to write binary files. Exception: {}.", e.what());
        return false;
    }
    modified_and_not_saved_ = false;
    return true;
}

bool Archive::LoadArchiveFromDirectory(const std::filesystem::path &path) {
    // Clear current archive.
    ClearArchive();
    std::error_code ecode;
    if(!exists(path, ecode)) {
        // TODO add logger
        // LOG_WARN("Directory {} does not exist. Error code: {}.", path, ecode.messsage());
        return false;
    }
    if(!is_directory(path, ecode)) {
        // TODO add logger
        // LOG_WARN("{} is not a directory. Error code: {}.", path, ecode.messsage());
        return false;
    }
    // Load merged blobs.
    std::vector<std::shared_ptr<MergedBlob> > merged_blobs;
    for(int blob_index = 1; ; ++blob_index) {
        auto binary_name = "Merged_" + std::to_string(blob_index) + ".pack";
        auto binary_path = path / binary_name;
        if(!exists(binary_path, ecode)) break;
        std::ifstream ifs(binary_path, std::ios::binary);
        if(ifs.bad()) {
            // TODO add logger
            // LOG_WARN("Failed to open file {} for reading.", binary_path);
            ClearArchive();
            return false;
        }
        auto merged_blob = std::make_shared<MergedBlob>();
        merged_blob->Deserialize(ifs);
        merged_blobs.emplace_back(merged_blob);
    }
    // TODO add logger
    // LOG_INFO("Loaded {} merged blobs.", merged_blobs.size());
    // Load non-merged blobs.
    std::function<bool(const std::filesystem::path &)> LoadNonMergedFrom = [&](const std::filesystem::path &path) {
        if(is_directory(path)) {
            return std::ranges::all_of(std::filesystem::directory_iterator(path), LoadNonMergedFrom);
        } else if(is_regular_file(path)) {
            auto file_name = path.filename();
            if(file_name.extension() == ".bin") {
                auto blob = std::make_shared<Blob>();
                std::ifstream ifs(path, std::ios::binary);
                if(ifs.bad()) {
                    // TODO add logger
                    // LOG_WARN("Failed to open file {} for reading.", path);
                    return false;
                }
                blob->Deserialize(ifs);
                blobs_[file_name.stem().string()] = blob;
                return true;
            }
        }
        return true;
    };
    if(!LoadNonMergedFrom(path)) {
        ClearArchive();
        return false;
    }
    // Extract merged blobs
    for (const auto &merged_blob: merged_blobs) {
        for (const auto &blob: merged_blob->blobs) {
            blobs_[blob->header.path] = blob;
        }
    }
    // TODO add logger
    // LOG_INFO("Archive holds {} blobs.", blobs_.size());
    modified_and_not_saved_ = false;
    return true;
}


CB_NAMESPACE_END